<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="keywords" content="C&#43;&#43;,Javascript,Node,Java,C#,编程,前端,Linux">
  <meta name="description" content="研究计算机程序设计">
  <meta name="author" content="leecjson">
  <meta property="og:title" content="使用JSDoc代替TypeScript实现类型检查" />
<meta property="og:description" content="TypeScript是个好东西，不过自从使用TypeScript以后带来了以下几个问题： 代码变的更加复杂 需要正确的设置一些非必要却很复杂的类" />
<meta property="og:type" content="article" />
<meta property="og:url" content="/cs/use-jsdoc-instead-of-typescript/" />
<meta property="article:published_time" content="2020-07-31T11:47:49+08:00" />
<meta property="article:modified_time" content="2020-07-31T11:47:49+08:00" />
<meta itemprop="name" content="使用JSDoc代替TypeScript实现类型检查">
<meta itemprop="description" content="TypeScript是个好东西，不过自从使用TypeScript以后带来了以下几个问题： 代码变的更加复杂 需要正确的设置一些非必要却很复杂的类">
<meta itemprop="datePublished" content="2020-07-31T11:47:49+08:00" />
<meta itemprop="dateModified" content="2020-07-31T11:47:49+08:00" />
<meta itemprop="wordCount" content="1854">



<meta itemprop="keywords" content="JavaScript,TypeScript," />
<meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="使用JSDoc代替TypeScript实现类型检查"/>
<meta name="twitter:description" content="TypeScript是个好东西，不过自从使用TypeScript以后带来了以下几个问题： 代码变的更加复杂 需要正确的设置一些非必要却很复杂的类"/>
<title>使用JSDoc代替TypeScript实现类型检查 | 杰森猫的电子博客</title>

  <link rel="stylesheet"
    href="../../dist/hash/fontawesome.84d8ad2b4fcdc0f0c58247e778133b3a/css/all.min.css">
  <link rel="stylesheet"
    href="../../dist/hash/main.db873ee70909ea865f5f.css">
</head>
<body>
  
  <header class="app__topbar elevation-z4">
    <a class="app__brand" href="../../">杰森猫的电子博客</a>
    <div class="app__topbar-link-list">
      <a target="_blank" class="app__topbar-link" href="https://github.com/leecjson"><i class="fab fa-github"></i></a>
    </div>
  </header>
  

  <main>
  <article class="markdown-body">
    <h1>使用JSDoc代替TypeScript实现类型检查</h1><p>TypeScript是个好东西，不过自从使用TypeScript以后带来了以下几个问题：</p>
<ol>
<li>代码变的更加复杂</li>
<li>需要正确的设置一些非必要却很复杂的类型</li>
<li>侵入JavaScript语法，可能带来向前兼容问题</li>
<li>运行前存在编译阶段，极大的降低开发体验</li>
</ol>
<p>其实很多人只想写Vanilla JavaScript代码，但只写纯JavaScript还是会存在各种问题，比较好的方式是至少加一个ESLint进来。写纯JavaScript代码的人有个非常普遍的核心诉求就是代码提示，另一个是也许不那么重要的类型检查，从来没有人说必须要有类型检查才能写出一份好代码，但类型检查至少保证不会犯一些低级的代码错误，这类低级错误通常都是修改或重构代码导致的，一个经验丰富的程序员通常不会一开始就写出含有低级错误的代码。使用Vanilla JavaScript + JSDoc的好处就是当需要关心类型时加类型，不需要关心时什么也不用做。</p>
<p>书归正传，使用<a href="https://jsdoc.app/">JSDoc</a>在JavaScript代码中来做类型检查是因为TypeScript某种程度上支持了JSDoc <a href="https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html">https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html</a>，另一方面由于增加了类型信息，可以让编辑器根据类型信息进行代码提示（Nice）。</p>
<h3 id="项目配置">项目配置</h3>
<p>拿VSCode来举例子，以下是项目根目录中<code>jsconfig.json</code>的内容</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-json" data-lang="json">{
  <span style="color:#f92672">&#34;compilerOptions&#34;</span>: {
    <span style="color:#f92672">&#34;target&#34;</span>: <span style="color:#e6db74">&#34;esnext&#34;</span>,
    <span style="color:#f92672">&#34;strict&#34;</span>: <span style="color:#66d9ef">true</span>,
    <span style="color:#f92672">&#34;noImplicitAny&#34;</span>: <span style="color:#66d9ef">true</span>,
    <span style="color:#f92672">&#34;strictNullChecks&#34;</span>: <span style="color:#66d9ef">true</span>,
    <span style="color:#f92672">&#34;checkJs&#34;</span>: <span style="color:#66d9ef">true</span>,
    <span style="color:#f92672">&#34;jsx&#34;</span>: <span style="color:#e6db74">&#34;react&#34;</span>
  }
}
</code></pre></div><p>注意到<code>checkJs: true</code>表示检查JavaScript代码，如果不设置默认是不检查的，而JSDoc中的类型信息仅帮助编辑器做智能提示。另外使用JSX的时候也是能跟JSDoc兼容的。</p>
<h3 id="类型声明语法">类型声明语法</h3>
<p>JSDoc中所有<code>{}</code>括号里的内容填写的是具体的类型，其语法构成其实就是TypeScript中的一个类型表达式，可以使用模板/泛型，复合类型&hellip;</p>
<h3 id="变量">变量</h3>
<h5 id="变量初始化时自动推断">变量初始化时自动推断：</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">str</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;   <span style="color:#75715e">// const str: string = &#39;&#39;;
</span></code></pre></div><h5 id="强制声明">强制声明</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * @type {number}
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">str</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;
</code></pre></div><p>报错<code>Type '&quot;&quot;' is not assignable to type 'number'.ts</code>，强制声明为<code>number</code>的变量不能被一个<code>string</code>字面量赋值</p>
<h5 id="声明为自定义类型">声明为自定义类型</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * @typedef {object} Book 书的类型定义
</span><span style="color:#75715e"> * @property {string} name 书名
</span><span style="color:#75715e"> * @property {number} price 价格
</span><span style="color:#75715e"> * @property {boolean=} sold 是否出售
</span><span style="color:#75715e"> */</span>

<span style="color:#75715e">/** @type {Book} */</span>
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">book</span>;
</code></pre></div><p>接下来变量<code>book</code>变量可以自动提示相应的属性，被赋值为其它object类型时会报错。注意到<code>@property {boolean=} sold</code>中的<code>{boolean=}</code>其实代表<code>{boolean|undefined}</code></p>
<h5 id="导入类型">导入类型</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * @type {import(&#39;./book&#39;)} 导入上一个文件中定义的Book类型
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">book</span>;
</code></pre></div><p>通过<code>import</code>语法不仅可以导入在JavaScript中由<code>@typedef</code>声明的类型，也可以导入第三方或Node核心库的Module中的类型，类型通常来源于TypeScript声明文件或源文件导出的。</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * @type {import(&#39;koa&#39;).Context} 导入koa包中的Context类型
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">context</span>;
</code></pre></div><h3 id="强制类型转换">强制类型转换</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">str</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;
<span style="color:#75715e">/** @type {number} */</span>
<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">num</span>;
<span style="color:#a6e22e">num</span> <span style="color:#f92672">=</span> (<span style="color:#75715e">/** @type {number} */</span> (<span style="color:#a6e22e">str</span>));
</code></pre></div><h3 id="方法">方法</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * Add Two Number
</span><span style="color:#75715e"> * @param {number} x First Number
</span><span style="color:#75715e"> * @param {number} y Second Number
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">x</span>, <span style="color:#a6e22e">y</span>) {
  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">x</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">y</span>;
}

<span style="color:#a6e22e">add</span>(<span style="color:#ae81ff">1</span>, <span style="color:#e6db74">&#39;fuck&#39;</span>);
</code></pre></div><p>最后一行代码会报<code>Argument of type '&quot;fuck&quot;' is not assignable to parameter of type 'number'.ts(2345)</code>，并且返回类型自动推断为<code>number</code></p>
<p>如果你尝试在<code>add</code>方法内写<code>a.xxx</code>会发现VSCode给你提示出<code>number</code>类型中的相关函数，例如<code>toFixed</code></p>
<p>另一种支持的写法：</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * Add Two Number
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">add</span>(<span style="color:#75715e">/**@type {number}*/</span> <span style="color:#a6e22e">x</span>, <span style="color:#75715e">/**@type {number}*/</span> <span style="color:#a6e22e">y</span>) {
  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">x</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">y</span>;
}
</code></pre></div><h5 id="异步方法">异步方法</h5>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * @returns {Promise&lt;number&gt;}
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">get</span>() {
  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Promise((<span style="color:#a6e22e">resolve</span>, <span style="color:#a6e22e">reject</span>) =&gt; {
    <span style="color:#a6e22e">resolve</span>(<span style="color:#ae81ff">16</span>);
  });
}
</code></pre></div><h5 id="对象参数类型">对象参数类型</h5>
<p>与<code>@typedef</code>语法很相似，只不过应用在了方法参数声明上</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * JSDoc风格的声明
</span><span style="color:#75715e"> * @param {object} props
</span><span style="color:#75715e"> * @param {number} props.a
</span><span style="color:#75715e"> * @param {number} props.b
</span><span style="color:#75715e"> * @param {object} props.sub
</span><span style="color:#75715e"> * @param {string} props.sub.name
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Func</span>(<span style="color:#a6e22e">props</span>) {
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ret</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">props</span>.<span style="color:#a6e22e">a</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">props</span>.<span style="color:#a6e22e">b</span>;
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">props</span>.<span style="color:#a6e22e">sub</span>.<span style="color:#a6e22e">name</span>;
}

<span style="color:#75715e">/**
</span><span style="color:#75715e"> * TypeScript风格的声明
</span><span style="color:#75715e"> * @param {{ a: number, b: number, sub: { name: string } }} props
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Func</span>(<span style="color:#a6e22e">props</span>) {
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ret</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">props</span>.<span style="color:#a6e22e">a</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">props</span>.<span style="color:#a6e22e">b</span>;
  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">props</span>.<span style="color:#a6e22e">sub</span>.<span style="color:#a6e22e">name</span>;
}
</code></pre></div><h3 id="继承一个模板类型">继承一个模板类型</h3>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e"> * @template T
</span><span style="color:#75715e"> * @extends {Set&lt;T&gt;}
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SortableSet</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Set</span> {
  <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}

<span style="color:#75715e">/**
</span><span style="color:#75715e"> * @extends {Set&lt;string&gt;}
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">StringSet</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Set</span> {
  <span style="color:#75715e">// ...
</span><span style="color:#75715e"></span>}
</code></pre></div><p>通过这样的方式可以使<code>SortableSet</code>把参数化模板延续下去，或者直接通过<code>@extends</code>语法指定一个模板类型使<code>Set&lt;T&gt;</code>成为一个特例模板<code>StringSet</code></p>
<h3 id="与typescript类型系统配合">与TypeScript类型系统配合</h3>
<p>JSDoc绝对能满足日常对于类型检查的需求，如果还不满足的话，另外可以使用TypeScript书写声明文件来更加精准的配合定义类型系统，而后在JSDoc中使用<code>import</code>语法导入。</p>
<h2 id="在reactjs中的实践">在React.js中的实践</h2>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#75715e">/**
</span><span style="color:#75715e">  @extends {React.Component&lt;{
</span><span style="color:#75715e">    history: import(&#39;history&#39;).History,
</span><span style="color:#75715e">    desc: Record&lt;string, any&gt;[],
</span><span style="color:#75715e">    level: number,
</span><span style="color:#75715e">    nestedPadding: number
</span><span style="color:#75715e">  }&gt;}
</span><span style="color:#75715e"> */</span>
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MenuItemList</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">React</span>.<span style="color:#a6e22e">Component</span> {

}
</code></pre></div><h2 id="总结">总结</h2>
<ol>
<li>TypeScript是优秀的</li>
<li>在JSDoc下，非侵入式的使用TypeScript是极好的</li>
<li>类型系统是有必要的</li>
<li>最好在VSCode下进行实践</li>
</ol>
<p>相关文献：</p>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html">https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html</a></li>
<li><a href="https://fettblog.eu/typescript-jsdoc-superpowers/">https://fettblog.eu/typescript-jsdoc-superpowers/</a></li>
<li><a href="https://www.youtube.com/watch?v=YHvqbeh_n9U">https://www.youtube.com/watch?v=YHvqbeh_n9U</a></li>
<li><a href="https://medium.com/@trukrs/type-safe-javascript-with-jsdoc-7a2a63209b76">https://medium.com/@trukrs/type-safe-javascript-with-jsdoc-7a2a63209b76</a></li>
</ul>
</article>
</main>

  <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
    integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
    crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
    integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
    crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/js/bootstrap.min.js"
    integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"
    crossorigin="anonymous"></script>
</body>

</html>